/*
    Coder : Dzlua
    Email : 505544956@qq.com
    Time  : 2017/07/04
*/
#include "player.h"
#include "../decoder/decoder.h"

extern "C" {

#include <sdl/SDL.h>
#include <sdl/SDL_TTF.h>

}

namespace dzlua {
namespace av {

class PlayerImpl
        : public Player {
public:
    PlayerImpl();
    ~PlayerImpl();
public:
    virtual bool Init() override;
    virtual void Quit() override;
    virtual void Run() override;
    virtual bool Open(const std::string &file) override;
    virtual void Close() override;
    virtual void Play() override;
    virtual void Stop() override;
    virtual void Pause() override;
    virtual void Continue() override;
    virtual void Seek(double pos) override;
protected:
    bool openVideo();
    bool openAudio();
protected:
    void setClock(double time);
    double getClock();
    // return millisecond
    int32_t delayTime(double time);
protected:
    std::shared_ptr<Decoder> decoder_;
    std::shared_ptr<SDL_Window> window_;
    std::shared_ptr<SDL_Renderer> render_;
    std::shared_ptr<SDL_Texture> texture_;
    std::shared_ptr<SDL_mutex> mutex_texture_;
    std::shared_ptr<SDL_TimerID> timer_video_;
    std::shared_ptr<TTF_Font> font_;

    std::string file_;
    SDL_AudioSpec audio_spec_;

    std::shared_ptr<VideoData> data_video_;
    std::shared_ptr<AudioData> data_audio_;
    
    std::atomic<double> clock_;
};

//-----------------------//
std::shared_ptr<Player> Player::NewInterface() {
    return std::make_shared<PlayerImpl>();
}

//-----------------------//
PlayerImpl::PlayerImpl()
        : decoder_(nullptr)
        , window_(nullptr)
        , render_(nullptr)
        , texture_(nullptr)
        , mutex_texture_(nullptr)
        , timer_video_(nullptr)
        , font_(nullptr)
        , data_video_(nullptr)
        , data_audio_(nullptr)
        , clock_(0) {
    decoder_ = Decoder::NewInterface();
}

PlayerImpl::~PlayerImpl() {
    this->Quit();
}

//-----------------------//
bool PlayerImpl::Init() {
    if (SDL_Init(SDL_INIT_EVERYTHING) != 0){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unable to initialize SDL2: %s", SDL_GetError());
        return false;
    }

    if (TTF_Init() != 0){
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unable to initialize TTF: %s", TTF_GetError());
        return false;
    }

    window_ = std::shared_ptr<SDL_Window>(
        SDL_CreateWindow("dzlua::av::player"
                , SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED
                , 640, 480, SDL_WINDOW_SHOWN)
        , [](SDL_Window* wnd) {
            SDL_DestroyWindow(wnd);
        });
    if (!window_) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unable to create SDL2 window: %s", SDL_GetError());
        return false;
    }

    render_ = std::shared_ptr<SDL_Renderer>(
        SDL_CreateRenderer(
            window_.get()
            , -1
            , SDL_RENDERER_ACCELERATED)
        , [](SDL_Renderer *render) {
            SDL_DestroyRenderer(render);
        });
    if (!render_) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unable to create SDL2 render: %s", SDL_GetError());
        return false;
    }

    mutex_texture_ = std::shared_ptr<SDL_mutex>(
          SDL_CreateMutex()
        , [](SDL_mutex *mutex) {
            SDL_DestroyMutex(mutex);
        });
    if (!mutex_texture_) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unable to create SDL2 texture mutex: %s", SDL_GetError());
        return false;
    }

    if (!decoder_ || !decoder_->Init()) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unable to init decoder.");
        return false;
    }

    font_ = std::shared_ptr<TTF_Font>(
          TTF_OpenFont("simhei.ttf", 20)
        , [](TTF_Font * font) {
            TTF_CloseFont(font);
        });
    if (!font_) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unable to create TTF_Font: %s", TTF_GetError());
        //return false;
    }

    return true;
}

void PlayerImpl::Quit() {
    this->Stop();
    decoder_ = nullptr;
    mutex_texture_ = nullptr;
    font_ = nullptr;    
    render_ = nullptr;
    window_ = nullptr;

    if (TTF_WasInit() == 0){
        TTF_Quit();
    }

    if (SDL_WasInit(SDL_INIT_EVERYTHING)) {
        SDL_Quit();
    }
}

void PlayerImpl::Run() {
    SDL_Event evt;
    while (true) {
        if (!SDL_WaitEvent(&evt)) continue;
        if (evt.type == SDL_QUIT) break;
        if (evt.type == SDL_KEYDOWN) {
            switch (evt.key.keysym.sym) {
                case SDLK_p: Pause(); break;
                case SDLK_c: Continue(); break;
                case SDLK_r: Play(); break;
                case SDLK_o: Open(file_); break;
                case SDLK_q: Close(); break;
                case SDLK_s: Stop(); break;
            }
        }
        if (evt.type == SDL_WINDOWEVENT) {
            if (evt.window.event == SDL_WINDOWEVENT_EXPOSED){
                if (!render_ || !texture_ || !mutex_texture_)
                    continue;

                SDL_RenderClear(render_.get());
                SDL_LockMutex(mutex_texture_.get());
                SDL_RenderCopy(render_.get(), texture_.get(), nullptr, nullptr);
                SDL_UnlockMutex(mutex_texture_.get());
                
                if (font_) {
                    SDL_Color color = { 255, 0, 0, 0};
                    SDL_Rect rect;
                    std::string text = std::to_string(this->getClock())
                            + "/" + std::to_string(this->decoder_->Duration());

                    SDL_Surface *surface = TTF_RenderUTF8_Blended(
                              font_.get()
                            , text.c_str()
                            , color);
                    SDL_Texture *texture = SDL_CreateTextureFromSurface(render_.get(), surface);

                    if (!surface || !texture)
                        continue;

                    rect.x = 0;
                    rect.y = 0;
                    rect.w = surface->w;
                    rect.h = surface->h;
                    SDL_RenderCopy(render_.get(), texture, nullptr, &rect);

                    SDL_FreeSurface(surface);
                    SDL_DestroyTexture(texture);
                }

                SDL_RenderPresent(render_.get());
            }
        }
    }
}

bool PlayerImpl::Open(const std::string &file) {
    this->Close();
    file_ = file;
    if (!decoder_->Open(file))
        return false;

    bool b1 = this->openVideo();
    bool b2 = this->openAudio();

    decoder_->SetNoUse(!b1, !b2, false);

    if (!b1 && !b2) {
        this->Close();
        return false;
    }

    return true;
}

bool PlayerImpl::openVideo() {
    // for video
    auto info = decoder_->getVideoInfo();
    if (!info) return false;

    //info->format = av::ePixelFormat::rgb24;
    info->format = av::ePixelFormat::yuv420p;
    info->algorithm = av::eAlgorithm::bicubic;
    //info->width = 640;
    //info->height = 480;
    decoder_->setInfo(info.get());

    SDL_SetWindowSize(window_.get(), info->width, info->height);

    texture_ = std::shared_ptr<SDL_Texture>(
        SDL_CreateTexture(
            render_.get()
            , SDL_PIXELFORMAT_IYUV /*SDL_PIXELFORMAT_RGB24*/
            , SDL_TEXTUREACCESS_STREAMING
            , info->width
            , info->height)
        , [](SDL_Texture *texture) {
            SDL_DestroyTexture(texture);
        });
    if (!texture_) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unable to create SDL2 texture: %s", SDL_GetError());
        return false;
    }

    timer_video_ = std::shared_ptr<SDL_TimerID>(
          new SDL_TimerID
        , [](SDL_TimerID* id) {
            SDL_RemoveTimer(*id);
            delete id;
        });
    if (!timer_video_) {
        texture_ = nullptr;
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "Unable to alloc SDL_TimerID.");
        return false;
    }

    data_video_ = nullptr;

    *timer_video_ = SDL_AddTimer(
          40
        , [](Uint32 interval, void *param) -> Uint32 {
            /*
            *   check pointer
            */
            PlayerImpl * pl = (PlayerImpl*)param;
            if (!pl->decoder_ || !pl->texture_ ||!pl->mutex_texture_)
                return 40;

            if (!pl->data_video_) {
                /*
                *   get data.
                */
                pl->data_video_ = pl->decoder_->pop_video_data();
                if (!pl->data_video_) return 40;

                av::log("#");
            }

            // init interval as 10.
            interval = 10;

            /*
            *   ensure the video synchronize with sound.
            */
            int32_t delay_time = pl->delayTime(pl->data_video_->time);
            if (delay_time > 1) {
                // video time is fast, need delay
                return interval;
            } else {
                // video time is the same as audio time.
                // video time is slow, show video quickly.

                /*
                *   set video data
                */
                SDL_LockMutex(pl->mutex_texture_.get());
                SDL_UpdateYUVTexture( pl->texture_.get()
                        , nullptr
                        , pl->data_video_->data[0], pl->data_video_->linesize[0]
                        , pl->data_video_->data[1], pl->data_video_->linesize[1]
                        , pl->data_video_->data[2], pl->data_video_->linesize[2] );
                /*SDL_UpdateTexture(pl->texture_.get()
                        , nullptr
                        , data->data[0], data->linesize[0]);*/
                SDL_UnlockMutex(pl->mutex_texture_.get());

                /*
                *   notify main thread to flush video.
                */
                SDL_Event evt;
                evt.type = SDL_WINDOWEVENT;
                evt.window.event = SDL_WINDOWEVENT_EXPOSED;
                SDL_PushEvent(&evt);

                // video time is slow, show video quickly.
                if (delay_time < -1) {
                    /*
                    *   release data
                    */
                    pl->data_video_ = nullptr;
                    return interval;
                }

                // video time is the same as audio time.
                if (pl->data_video_->delay > 0
                        && pl->data_video_->delay < 50) {
                    /*
                    *   release data
                    */
                    interval = pl->data_video_->delay;
                    pl->data_video_ = nullptr;
                    return interval;
                }
            
            }

            return interval;
        }, this);
    if (*timer_video_ == 0) {
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "can't add video timer. SDL2: %s \n", SDL_GetError());
        timer_video_ = nullptr;
        texture_ = nullptr;
        return false;
    }

    return true;
}

bool PlayerImpl::openAudio() {
    // for audio
    auto info = decoder_->getAudioInfo();
    if (!info) return false;

    info->layout = av::eChannelLayout::stereo;
    info->format = av::eSampleFormat::s16;
    /*
    *   32000->44100
    *   to pcm file -> wav file -> media player can play correctly.
    *   but in there, some thing error,if set to 44100.
    */
    //info->rate = 44100;
    info->channels = 2;
    decoder_->setInfo(info.get());

    data_audio_ = nullptr;

    audio_spec_.freq = info->rate;   
    audio_spec_.format = AUDIO_S16SYS;
    audio_spec_.channels = info->channels;   
    audio_spec_.silence = 0;   
    audio_spec_.samples = info->samples;   
    audio_spec_.userdata = this;
    audio_spec_.callback = 
        [](void *userdata, Uint8 * stream, int len) {
            SDL_memset(stream, 0, len);
            PlayerImpl * pl = (PlayerImpl*)userdata;
            static int audio_len = 0;
            static uint8_t* audio_pos = nullptr;
            if (0 >= audio_len) {
                if (!pl->decoder_) return;
                pl->data_audio_ = pl->decoder_->pop_audio_data();
                if (!pl->data_audio_) return;

                av::log("*");

                pl->setClock(pl->data_audio_->time);

                audio_pos = pl->data_audio_->data;
                audio_len = pl->data_audio_->size;
                
                if (audio_len == 0) {
                    pl->data_audio_ = nullptr;
                    return;
                }
            }

            len = ((len > audio_len) ? audio_len : len);
            SDL_MixAudio(stream, audio_pos, len, SDL_MIX_MAXVOLUME);
            audio_pos += len;   
            audio_len -= len;
        };
  
    if (SDL_OpenAudio(&audio_spec_, nullptr) < 0){   
        SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "can't open audio. SDL2: %s \n", SDL_GetError());
        return false;
    }
    return true;
}

void PlayerImpl::setClock(double time) {
    clock_ = time;
}

double PlayerImpl::getClock() {
    return clock_;
}

int32_t PlayerImpl::delayTime(double time) {
    return (int32_t)((time - clock_) * 1000);
}

void PlayerImpl::Close() {
    SDL_CloseAudio();
    decoder_->Close();
    timer_video_ = nullptr;
    texture_ = nullptr;
    data_audio_ = nullptr;
    data_video_ = nullptr;
    clock_ = 0;
}

void PlayerImpl::Play() {
    decoder_->Run();
    SDL_PauseAudio(0);
}

void PlayerImpl::Stop() {
    this->Close();
    decoder_->Stop();
}

void PlayerImpl::Pause() {
    SDL_PauseAudio(1);
    decoder_->Pause();
}

void PlayerImpl::Continue() {
    SDL_PauseAudio(0);
    decoder_->Continue();
}

void PlayerImpl::Seek(double pos) {
    decoder_->Seek(pos);
}

//-----------------------//

} // end namespace av
} // end namespace dzlua